/**
 * @author Stéphane LaFlèche <stephane.l@vanillaforums.com>
 * @copyright 2009-2023 Vanilla Forums Inc.
 * @license GPL-2.0-only
 */

import ModalSizes from "@library/modal/ModalSizes";
import { ModalView } from "@library/modal/ModalView";
import { logWarning, debug } from "@vanilla/utils";
import React from "react";
import { TabHandler } from "@vanilla/dom-utils";
import { mountPortal } from "@vanilla/react-utils";
import { uniqueIDFromPrefix } from "@library/utility/idUtils";
import { MODAL_CONTAINER_ID, PAGE_CONTAINER_ID } from "@library/modal/mountModal";
import { ModalContext } from "@library/modal/Modal.context";
import { isVisible } from "@testing-library/user-event/dist/cjs/utils/index.js";

interface IModalProps {
    id?: string;
    className?: string;
    exitHandler?: (event?: React.SyntheticEvent<any>) => void;
    pageContainer?: Element | null;
    container?: Element;
    description?: string;
    children: React.ReactNode;
    elementToFocus?: HTMLElement;
    size: ModalSizes;
    scrollable?: boolean;
    noFocusOnExit?: boolean;
    elementToFocusOnExit?: HTMLElement; // Should either be a specific element or use document.activeElement
    isWholePage?: boolean;
    isVisible: boolean;
    afterContent?: React.ReactNode;
    titleID?: string;
    label?: string; // Necessary if there's no proper title
    onKeyPress?: (e) => void;
    isFixHeight?: boolean; // If enabled, regardless of content, modal's height will be fixed to max
}

interface IState {
    wasDestroyed: boolean;
    realRootID: string;
}

/**
 * An accessible Modal component.
 *
 * - Renders into the `#modals` element with a React portal.
 * - Implements tab trapping.
 * - Closes with the escape key.
 * - Sets aria-hidden on the main application.
 * - Prevents scrolling of the body.
 * - Focuses the first focusable element in the Modal.
 */
export default class Modal extends React.Component<IModalProps, IState> {
    public static stack: Modal[] = [];
    private selfRef = React.createRef<HTMLDivElement>();

    public static defaultProps = {
        isVisible: true,
    };

    public state: IState = {
        wasDestroyed: !this.props.isVisible,
        realRootID: uniqueIDFromPrefix("realModalRoot"),
    };

    /**
     * Render the contents into a portal.
     */
    public render() {
        if (this.state.wasDestroyed) {
            return null;
        }

        const portal = mountPortal(
            <>
                <ModalView
                    id={this.props.id}
                    className={this.props.className}
                    onDestroyed={this.handleDestroyed}
                    scrollable={this.props.scrollable}
                    onKeyDown={this.handleTabbing}
                    onModalClick={this.handleModalClick}
                    onExit={this.onExit}
                    description={this.props.description}
                    size={this.props.size}
                    modalRef={this.selfRef}
                    titleID={"titleID" in this.props ? this.props.titleID : undefined}
                    label={"label" in this.props ? this.props.label : undefined}
                    isVisible={this.props.isVisible}
                    onKeyPress={this.props.onKeyPress}
                    realRootID={this.state.realRootID}
                    isFixHeight={this.props.isFixHeight}
                    elementToFocusOnExit={this.props.elementToFocusOnExit}
                    noFocusOnExit={this.props.noFocusOnExit}
                >
                    <ModalContext.Provider value={{ isInModal: true }}>{this.props.children}</ModalContext.Provider>
                </ModalView>
                {this.props.afterContent}
            </>,
            MODAL_CONTAINER_ID,
            true,
        );
        return (
            <>
                {/* This is just here so we can find where a modal really came from in the dom. */}
                <span id={this.state.realRootID} style={{ display: "none" }} />
                {portal}
            </>
        );
    }

    /**
     * Get a fresh instance of the TabHandler.
     *
     * Since the contents of the modal could be changing constantly
     * we are creating a new instance every time we need it.
     */
    private get tabHandler(): TabHandler | null {
        return this.selfRef.current ? new TabHandler(this.selfRef.current) : null;
    }

    /**
     * Initial component setup.
     *
     * Everything here should be torn down in componentWillUnmount
     */
    public componentDidMount() {
        const pageContainer = this.getPageContainer();
        if (!pageContainer) {
            logWarning(`
A modal was mounted, but the page container could not be found.
Please wrap your primary content area with the ID "${PAGE_CONTAINER_ID}" so it can be hidden to screenreaders.
            `);
        }
    }

    public onMountIn = () => {
        const pageContainer = this.getPageContainer();
        this.focusInitialElement();
        pageContainer && pageContainer.setAttribute("aria-hidden", "true");

        Modal.stack.push(this);
    };

    public handleDestroyed = () => {
        // Do some quick state updates to bump the modal to the top of the portal stack.
        // When we set this to true we render null once.
        // The in the update we set back to false.
        // The second render will re-create the portal.
        this.setState({ wasDestroyed: true });

        const pageContainer = this.getPageContainer();
        // Set aria-hidden on page and reenable scrolling if we're removing the last modal
        Modal.stack.pop();
        if (Modal.stack.length === 0) {
            pageContainer && pageContainer.removeAttribute("aria-hidden");
        } else {
            pageContainer && pageContainer.setAttribute("aria-hidden", "true");
        }
    };

    public componentDidUpdate(prevProps: IModalProps, prevState: IState) {
        if (prevState.wasDestroyed && !this.state.wasDestroyed) {
            this.onMountIn();
        }

        if (!prevProps.isVisible && this.props.isVisible) {
            this.setState({ wasDestroyed: false });
        }
    }

    private getPageContainer(): HTMLElement | null {
        return document.getElementById(PAGE_CONTAINER_ID);
    }

    /**
     * Focus the initial element in the Modal.
     */
    private focusInitialElement() {
        const focusElement = this.props.elementToFocus ? this.props.elementToFocus : this.tabHandler?.getInitial();
        if (focusElement) {
            focusElement!.focus();
        }
    }

    /**
     * Handle tab keyboard presses.
     */
    private handleTabbing = (event: React.KeyboardEvent) => {
        const tabKey = 9;

        if (event.shiftKey && event.keyCode === tabKey) {
            this.handleShiftTab(event);
        } else if (!event.shiftKey && event.keyCode === tabKey) {
            this.handleTab(event);
        }
    };

    /**
     * Stop propagation of events at the top of the modal so they don't make it
     * to the scrim click handler.
     */
    private handleModalClick = (event: React.MouseEvent) => {
        event.stopPropagation();
    };

    /**
     * Call the exit handler when the scrim is clicked directly.
     */
    private onExit = (event: React.MouseEvent) => {
        event.preventDefault();
        event.stopPropagation();
        if (this.props.exitHandler) {
            this.props.exitHandler(event);
        }
    };

    /**
     * Handle shift tab key presses.
     *
     * - Focuses the previous element in the modal.
     * - Loops if we are at the beginning
     *
     * @param event The react event.
     */
    private handleShiftTab(event: React.KeyboardEvent) {
        const previousElement = this.tabHandler?.getNext(undefined, true);
        if (previousElement) {
            event.preventDefault();
            event.stopPropagation();
            previousElement.focus();
        }
    }

    /**
     * Handle tab key presses.
     *
     * - Focuses the next element in the modal.
     * - Loops if we are at the end.
     *
     * @param event The react event.
     */
    private handleTab(event: React.KeyboardEvent) {
        const nextElement = this.tabHandler?.getNext();
        if (nextElement) {
            event.preventDefault();
            event.stopPropagation();
            nextElement.focus();
        }
    }
}
